import extractHtml from './source';
import { execScripts } from './source/scripts';
import { appStates, lifeCycles, keepAliveStates } from './constants';
import SandBox from './sandbox';
import {
  isFunction,
  cloneContainer,
  isBoolean,
  isPromise,
  logError,
  getRootContainer,
} from './libs/utils';
import dispatchLifecyclesEvent, {
  dispatchCustomEventToMicroApp,
} from './interact/lifecycles_event';
import globalEnv from './libs/global_env';
import { releasePatchSetAttribute } from './source/patch';
import { getActiveApps } from './micro_app';
import { Deferred } from './custom-utils';

// micro app instances
export const appInstanceMap = new Map();

export default class CreateApp {
  state = appStates.NOT_LOADED;
  keepAliveState = null;
  keepAliveContainer = null;
  loadSourceLevel = 0;
  umdHookMount = null;
  umdHookUnmount = null;
  libraryName = null;
  umdMode = false;
  isPrefetch = false;
  prefetchResolve = null;
  name;
  url;
  ssrUrl;
  container = null;
  inline;
  scopecss;
  useSandbox;
  baseroute = '';
  source;
  sandBox = null;
  // TODO: 渲染事件
  mountDeferred = new Deferred();
  mountPromise = this.mountDeferred.promise;
  unmountDeferred = new Deferred();
  unmountPromise = this.unmountDeferred.promise;

  constructor({ name, url, ssrUrl, container, inline, scopecss, useSandbox, baseroute, props }) {
    this.props = props;
    this.container = container ?? null;
    this.inline = inline ?? false;
    this.baseroute = baseroute ?? '';
    this.ssrUrl = ssrUrl ?? '';
    // optional during init👆
    this.name = name;
    this.url = url;
    this.useSandbox = useSandbox;
    this.scopecss = this.useSandbox && scopecss;
    this.source = {
      links: new Map(),
      scripts: new Map(),
    };
    this.loadSourceCode();
    this.useSandbox && (this.sandBox = new SandBox(name, url));
  }

  // Load resources
  loadSourceCode() {
    this.state = appStates.LOADING_SOURCE_CODE;
    extractHtml(this);
  }

  /**
   * When resource is loaded, mount app if it is not prefetch or unmount
   */
  onLoad(html) {
    if (++this.loadSourceLevel === 2) {
      this.source.html = html;

      if (this.isPrefetch) {
        this.prefetchResolve?.();
        this.prefetchResolve = null;
      } else if (appStates.UNMOUNT !== this.state) {
        this.state = appStates.LOAD_SOURCE_FINISHED;
        this.mount();
      }
    }
  }

  /**
   * Error loading HTML
   * @param e Error
   */
  onLoadError(e) {
    this.loadSourceLevel = -1;
    if (this.prefetchResolve) {
      this.prefetchResolve();
      this.prefetchResolve = null;
    }

    if (appStates.UNMOUNT !== this.state) {
      this.onerror(e);
      this.state = appStates.LOAD_SOURCE_ERROR;
    }
  }

  /**
   * mount app
   * @param container app container
   * @param inline js runs in inline mode
   * @param baseroute route prefix, default is ''
   */
  mount(container, inline, baseroute) {
    if (isBoolean(inline) && inline !== this.inline) {
      this.inline = inline;
    }

    this.container = this.container ?? container;
    this.baseroute = baseroute ?? this.baseroute;

    if (this.loadSourceLevel !== 2) {
      this.state = appStates.LOADING_SOURCE_CODE;
      return;
    }

    dispatchLifecyclesEvent(this.container, this.name, lifeCycles.BEFOREMOUNT);

    this.state = appStates.MOUNTING;

    cloneContainer(this.source.html, this.container, !this.umdMode);

    this.sandBox?.start(this.baseroute);

    let umdHookMountResult; // result of mount function

    if (!this.umdMode) {
      let hasDispatchMountedEvent = false;
      // if all js are executed, param isFinished will be true
      execScripts(this.source.scripts, this, (isFinished) => {
        if (!this.umdMode) {
          const { mount, unmount } = this.getUmdLibraryHooks();
          // if mount & unmount is function, the sub app is umd mode
          if (isFunction(mount) && isFunction(unmount)) {
            this.umdHookMount = mount;
            this.umdHookUnmount = unmount;
            this.umdMode = true;
            this.sandBox?.recordUmdSnapshot();
            try {
              // TODO: mount传值
              umdHookMountResult = this.umdHookMount({
                container: this.container,
                props: this.props,
              });
            } catch (e) {
              logError('an error occurred in the mount function \n', this.name, e);
            }
          }
        }

        if (!hasDispatchMountedEvent && (isFinished === true || this.umdMode)) {
          hasDispatchMountedEvent = true;
          this.handleMounted(umdHookMountResult);
        }
      });
    } else {
      this.sandBox?.rebuildUmdSnapshot();
      try {
        umdHookMountResult = this.umdHookMount?.();
      } catch (e) {
        logError('an error occurred in the mount function \n', this.name, e);
      }
      this.handleMounted(umdHookMountResult);
    }
  }

  /**
   * handle for promise umdHookMount
   * @param umdHookMountResult result of umdHookMount
   */
  handleMounted(umdHookMountResult) {
    // TODO: mount事件
    this.mountDeferred.resolve();
    if (isPromise(umdHookMountResult)) {
      umdHookMountResult.then(() => this.dispatchMountedEvent()).catch((e) => this.onerror(e));
    } else {
      this.dispatchMountedEvent();
    }
  }

  /**
   * dispatch mounted event when app run finished
   */
  dispatchMountedEvent() {
    if (appStates.UNMOUNT !== this.state) {
      this.state = appStates.MOUNTED;
      dispatchLifecyclesEvent(this.container, this.name, lifeCycles.MOUNTED);
    }
  }

  /**
   * unmount app
   * @param destroy completely destroy, delete cache resources
   * @param unmountcb callback of unmount
   */
  unmount(destroy, unmountcb) {
    if (this.state === appStates.LOAD_SOURCE_ERROR) {
      destroy = true;
    }

    this.state = appStates.UNMOUNT;
    this.keepAliveState = null;
    this.keepAliveContainer = null;

    // result of unmount function
    let umdHookUnmountResult;
    /**
     * send an unmount event to the micro app or call umd unmount hook
     * before the sandbox is cleared
     */
    if (this.umdHookUnmount) {
      try {
        umdHookUnmountResult = this.umdHookUnmount();
      } catch (e) {
        logError('an error occurred in the unmount function \n', this.name, e);
      }
    }

    // dispatch unmount event to micro app
    dispatchCustomEventToMicroApp('unmount', this.name);

    this.handleUnmounted(destroy, umdHookUnmountResult, unmountcb);
  }

  /**
   * handle for promise umdHookUnmount
   * @param destroy completely destroy, delete cache resources
   * @param umdHookUnmountResult result of umdHookUnmount
   * @param unmountcb callback of unmount
   */
  handleUnmounted(destroy, umdHookUnmountResult, unmountcb) {
    // TODO: unmount事件
    this.unmountDeferred.resolve();
    if (isPromise(umdHookUnmountResult)) {
      umdHookUnmountResult
        .then(() => this.actionsForUnmount(destroy, unmountcb))
        .catch(() => this.actionsForUnmount(destroy, unmountcb));
    } else {
      this.actionsForUnmount(destroy, unmountcb);
    }
  }

  /**
   * actions for unmount app
   * @param destroy completely destroy, delete cache resources
   * @param unmountcb callback of unmount
   */
  actionsForUnmount(destroy, unmountcb) {
    if (destroy) {
      this.actionsForCompletelyDestroy();
    } else if (this.umdMode && this.container.childElementCount) {
      cloneContainer(this.container, this.source.html, false);
    }

    // this.container maybe contains micro-app element, stop sandbox should exec after cloneContainer
    this.sandBox?.stop();
    if (!getActiveApps().length) {
      releasePatchSetAttribute();
    }

    // dispatch unmount event to base app
    dispatchLifecyclesEvent(this.container, this.name, lifeCycles.UNMOUNT);

    this.container.innerHTML = '';
    this.container = null;

    unmountcb && unmountcb();
  }

  // actions for completely destroy
  actionsForCompletelyDestroy() {
    if (!this.useSandbox && this.umdMode) {
      delete window[this.libraryName];
    }
    appInstanceMap.delete(this.name);
  }

  // hidden app when disconnectedCallback called with keep-alive
  hiddenKeepAliveApp() {
    const oldContainer = this.container;

    cloneContainer(
      this.container,
      this.keepAliveContainer
        ? this.keepAliveContainer
        : (this.keepAliveContainer = document.createElement('div')),
      false,
    );

    this.container = this.keepAliveContainer;

    this.keepAliveState = keepAliveStates.KEEP_ALIVE_HIDDEN;

    // event should dispatch before clone node
    // dispatch afterhidden event to micro-app
    dispatchCustomEventToMicroApp('appstate-change', this.name, {
      appState: 'afterhidden',
    });

    // dispatch afterhidden event to base app
    dispatchLifecyclesEvent(oldContainer, this.name, lifeCycles.AFTERHIDDEN);
  }

  // show app when connectedCallback called with keep-alive
  showKeepAliveApp(container) {
    // dispatch beforeshow event to micro-app
    dispatchCustomEventToMicroApp('appstate-change', this.name, {
      appState: 'beforeshow',
    });

    // dispatch beforeshow event to base app
    dispatchLifecyclesEvent(container, this.name, lifeCycles.BEFORESHOW);

    cloneContainer(this.container, container, false);

    this.container = container;

    this.keepAliveState = keepAliveStates.KEEP_ALIVE_SHOW;

    // dispatch aftershow event to micro-app
    dispatchCustomEventToMicroApp('appstate-change', this.name, {
      appState: 'aftershow',
    });

    // dispatch aftershow event to base app
    dispatchLifecyclesEvent(this.container, this.name, lifeCycles.AFTERSHOW);
  }

  /**
   * app rendering error
   * @param e Error
   */
  onerror(e) {
    dispatchLifecyclesEvent(this.container, this.name, lifeCycles.ERROR, e);
  }

  // get app state
  getAppState() {
    return this.state;
  }

  // get keep-alive state
  getKeepAliveState() {
    return this.keepAliveState;
  }

  // get umd library, if it not exist, return empty object
  getUmdLibraryHooks() {
    // after execScripts, the app maybe unmounted
    if (appStates.UNMOUNT !== this.state) {
      const global = this.sandBox?.proxyWindow ?? globalEnv.rawWindow;
      this.libraryName =
        getRootContainer(this.container).getAttribute('library') || `micro-app-${this.name}`;
      // do not use isObject
      return typeof global[this.libraryName] === 'object' ? global[this.libraryName] : {};
    }

    return {};
  }
}
